package mycollections.map;

import mycollections.MyConcurrentModificationException;

import java.util.Arrays;
import java.util.Iterator;


public class MyHashMap<K, V> implements MyMap<K, V> {
    private static final double DEFAULT_LOAD_FACTOR = 0.75;
    private static final int DEFAULT_CAPACITY = 16;
    private static final int MAX_CAPACITY = 1 << 16;
    private static final double MAX_LOAD_FACTOR = 1;

    private MyEntry<K, V> table[];
    private int size;
    private int capacity = DEFAULT_CAPACITY;
    private double loadFactor = DEFAULT_LOAD_FACTOR;
    private int threshold;
    private int modificationCount;

    class MyMapIterator implements Iterator<MyMap.Entry<K, V>> {
        private int visited;
        private int currentIndex;
        private MyEntry<K, V> currentEntry;
        private int expectedModificationCount;
        MyMapIterator() {
            expectedModificationCount=modificationCount;
            visited = 0;
            currentIndex = 0;
            currentEntry = table[currentIndex];
        }

        private void checkComodification(){
            if (expectedModificationCount != modificationCount)
                throw new MyConcurrentModificationException();
        }

        @Override
        public boolean hasNext() {
            checkComodification();
            return visited != size;
        }

        @Override
        public Entry<K, V> next() {
            checkComodification();
            if (currentEntry == null) {
                while (table[currentIndex] == null)
                    currentIndex++;
                currentEntry = table[currentIndex];
            }
            MyEntry<K,V> prevEntry = currentEntry;
            currentEntry = currentEntry.next;
            if (currentEntry ==null)
                currentIndex++;
            visited++;
            return prevEntry;
        }

        @Override
        public void remove() {
            checkComodification();
            Entry<K,V> oldEntry=next();
            MyHashMap.this.remove(oldEntry.getKey());
            expectedModificationCount=modificationCount;
        }
    }

    private static class MyEntry<K, V> implements Entry<K, V> {
        private K key;
        private V value;
        private MyEntry<K, V> next;

        private MyEntry(K key, V value) {
            this.key = key;
            this.value = value;
            next = null;
        }

        private MyEntry(K key, V value, MyEntry<K,V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public int hashCode() {
            return (key == null ? 0 : key.hashCode()) ^
                    (value == null ? 0 : value.hashCode());
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof MyEntry))
                return false;
            else {
                MyEntry<K,V> entry = (MyEntry<K,V>) obj;
                Object k = entry.getKey();
                if (key == k || (key != null && key.equals(k))) {
                    Object v = entry.getValue();
                    if (value == v || (value != null && value.equals(v)))
                        return true;
                }
                return false;
            }
        }
    }

    public MyHashMap() {
        init();
    }

    public MyHashMap(int capacity) {
        checkCapacity(capacity);
        this.capacity = capacity;
        init();

    }

    public MyHashMap(int capacity, double loadFactor) {
        checkCapacity(capacity);
        checkLoadFactor(loadFactor);
        this.capacity = capacity;
        this.loadFactor = loadFactor;
        init();
    }

    private boolean isEqualsKeys(K k1, K k2){
        return (k1==k2) || (k1==null? false: k1.equals(k2));
    }

    private void init(){
        modificationCount=0;
        table=new MyEntry[capacity];
        updateThreshold();
        size=0;
    }

    private void updateThreshold() {
        threshold = (int) (capacity * loadFactor);
    }

    private void resize(int newCapacity) {
        if (table.length == MAX_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        MyEntry<K, V> newTable[] = new MyEntry[newCapacity];
        transfer(newTable);
        table = newTable;
        updateThreshold();

    }

    private void transfer(Entry[] newTable) {
        for (Entry<K, V> entry : this) {
            int index = indexFor(hash(entry.getKey().hashCode()), capacity);
            if (newTable[index] == null)
                newTable[index] = new MyEntry<K, V>(entry.getKey(), entry.getValue());
            else
                newTable[index] = new MyEntry<K, V>(entry.getKey(), entry.getValue(), (MyEntry) newTable[index]);
        }
    }

    private void checkCapacity(int capacity) {
        if (capacity < 0 || capacity > MAX_CAPACITY)
            throw new IllegalArgumentException();
    }

    private void checkLoadFactor(double loadFactor) {
        if (loadFactor <= 0 && loadFactor > MAX_LOAD_FACTOR)
            throw new IllegalArgumentException();
    }

    private static int hash(Object o) {
        int h=0;
        if (o!=null) h=o.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    private static int indexFor(int h, int length) {
        return h & (length - 1);
    }

    private MyEntry<K, V> getEntry(K key) {
        int index = indexFor(hash(key), capacity);
        MyEntry<K, V> current = table[index];
        while (current != null) {
            if (isEqualsKeys(current.getKey(),key))
                return current;
            current=current.next;
        }
        return null;
    }

    @Override
    public void clear() {
        size = 0;
        Arrays.fill(table, null);
        modificationCount++;
    }

    @Override
    public boolean containsKey(K key) {
        return getEntry(key) != null;
    }

    @Override
    public boolean containsValue(V value) {
        for (Entry<K, V> entry : this) {
            V v = entry.getValue();
            if ((v != null && v.equals(value)) || (v==null && value==null))
                return true;
        }
        return false;
    }

    @Override
    public V get(K key) {
        MyEntry<K, V> entry = getEntry(key);
        if (entry != null)
            return entry.getValue();
        else return null;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public V put(K key, V value) {
        if (size>threshold)
            resize(Math.min(MAX_CAPACITY,capacity*2));
        MyEntry<K, V> entry = getEntry(key);
        modificationCount++;
        if (entry == null) {
            int index = indexFor(hash(key), capacity);
            table[index] = new MyEntry<K, V>(key, value,  table[index]);
            size++;
            return null;
        } else {
            V oldValue = entry.getValue();
            entry.setValue(value);
            return oldValue;
        }

    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public V remove(K key) {
        int index = indexFor(key.hashCode(), capacity);
        MyEntry<K, V> current = table[index];
        V value = null;
        if (current != null) {
            if (current.getKey().equals(key)) {
                value = current.getValue();
                table[index] = current.next;
                modificationCount++;
                size--;
            } else {
                while (current.next != null && !isEqualsKeys(current.next.getKey(),key))
                    current = current.next;
                if (current.next != null) {
                    value = current.next.getValue();
                    current.next = current.next.next;
                    modificationCount++;
                    size--;
                }

            }
        }
        return value;
    }

    @Override
    public Iterator<Entry<K, V>> iterator() {
        return new MyMapIterator();
    }
}
